/* Copyright (c) 2017 Rob King
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *   * Neither the name of the copyright holder nor the
 *     names of contributors may be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS,
 * COPYRIGHT HOLDERS, OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
#include "tmt_test.hpp"

#include <limits.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BUF_MAX   100
#define PAR_MAX   8
#define TAB       8
#define MAX(x, y) (((size_t)(x) > (size_t)(y)) ? (size_t)(x) : (size_t)(y))
#define MIN(x, y) (((size_t)(x) < (size_t)(y)) ? (size_t)(x) : (size_t)(y))
#define CLINE(vt) (vt)->screen.lines[MIN((vt)->curs.r, (vt)->screen.nline - 1)]

#define P0(x)        (vt->pars[x])
#define P1(x)        (vt->pars[x] ? vt->pars[x] : 1)
#define CB(vt, m, a) ((vt)->cb ? (vt)->cb(m, vt, a, (vt)->p) : (void)0)
#define INESC        ((vt)->state)

#define COMMON_VARS           \
  TMTSCREEN *s = &vt->screen; \
  TMTPOINT * c = &vt->curs;   \
  TMTLINE *  l = CLINE(vt);   \
  TMTCHAR *  t = vt->tabs->chars

#define HANDLER(name)         \
  static void name(TMT *vt) { \
    COMMON_VARS;

enum State { S_NUL, S_ESC, S_ARG };

struct TMT {
  TMTPOINT curs, oldcurs;
  TMTATTRS attrs, oldattrs;

  bool      dirty, acs, ignored;
  TMTSCREEN screen;
  TMTLINE * tabs;

  TMTCALLBACK    cb;
  void *         p;
  const wchar_t *acschars;

  mbstate_t ms;
  size_t    nmb;
  char      mb[BUF_MAX + 1];

  size_t     pars[PAR_MAX];
  size_t     npar;
  size_t     arg;
  enum State state;
};

static TMTATTRS defattrs = {false, false, false, false, false, false, TMT_COLOR_DEFAULT, TMT_COLOR_DEFAULT};
static void     writecharatcurs(TMT *vt, wchar_t w);

static wchar_t tacs(const TMT *vt, unsigned char c) {
  /* The terminfo alternate character set for ANSI. */
  static unsigned char map[]
      = {0020U, 0021U, 0030U, 0031U, 0333U, 0004U, 0261U, 0370U, 0361U, 0260U, 0331U, 0277U, 0332U, 0300U, 0305U, 0176U,
         0304U, 0304U, 0304U, 0137U, 0303U, 0264U, 0301U, 0302U, 0263U, 0363U, 0362U, 0343U, 0330U, 0234U, 0376U};
  for (size_t i = 0; i < sizeof(map); i++)
    if (map[i] == c)
      return vt->acschars[i];
  return (wchar_t)c;
}

static void dirtylines(TMT *vt, size_t s, size_t e) {
  vt->dirty = true;
  for (size_t i = s; i < e; i++) vt->screen.lines[i]->dirty = true;
}

static void clearline(TMT *vt, TMTLINE *l, size_t s, size_t e) {
  vt->dirty = l->dirty = true;
  for (size_t i = s; i < e && i < vt->screen.ncol; i++) {
    l->chars[i].a = defattrs;
    l->chars[i].c = L' ';
  }
}

static void clearlines(TMT *vt, size_t r, size_t n) {
  for (size_t i = r; i < r + n && i < vt->screen.nline; i++) clearline(vt, vt->screen.lines[i], 0, vt->screen.ncol);
}

static void scrup(TMT *vt, size_t r, size_t n) {
  n = MIN(n, vt->screen.nline - 1 - r);

  if (n) {
    TMTLINE *buf[n];

    memcpy(buf, vt->screen.lines + r, n * sizeof(TMTLINE *));
    memmove(vt->screen.lines + r, vt->screen.lines + r + n, (vt->screen.nline - n - r) * sizeof(TMTLINE *));
    memcpy(vt->screen.lines + (vt->screen.nline - n), buf, n * sizeof(TMTLINE *));

    clearlines(vt, vt->screen.nline - n, n);
    dirtylines(vt, r, vt->screen.nline);
  }
}

static void scrdn(TMT *vt, size_t r, size_t n) {
  n = MIN(n, vt->screen.nline - 1 - r);

  if (n) {
    TMTLINE *buf[n];

    memcpy(buf, vt->screen.lines + (vt->screen.nline - n), n * sizeof(TMTLINE *));
    memmove(vt->screen.lines + r + n, vt->screen.lines + r, (vt->screen.nline - n - r) * sizeof(TMTLINE *));
    memcpy(vt->screen.lines + r, buf, n * sizeof(TMTLINE *));

    clearlines(vt, r, n);
    dirtylines(vt, r, vt->screen.nline);
  }
}

HANDLER(ed)
(void)t;

size_t b = 0;
size_t e = s->nline;

switch (P0(0)) {
  case 0:
    b = c->r + 1;
    clearline(vt, l, c->c, vt->screen.ncol);
    break;
  case 1:
    e = c->r - 1;
    clearline(vt, l, 0, c->c);
    break;
  case 2: /* use defaults */ break;
  default: /* do nothing   */ return;
}

clearlines(vt, b, e - b);
}

HANDLER(ich)
(void)t;

size_t n = P1(0); /* XXX use MAX */
if (n > s->ncol - c->c - 1)
  n = s->ncol - c->c - 1;

memmove(l->chars + c->c + n, l->chars + c->c, MIN(s->ncol - 1 - c->c, (s->ncol - c->c - n - 1)) * sizeof(TMTCHAR));
clearline(vt, l, c->c, n);
}

HANDLER(dch)
(void)t;

size_t n = P1(0); /* XXX use MAX */
if (n > s->ncol - c->c)
  n = s->ncol - c->c;
else if (n == 0)
  return;

memmove(l->chars + c->c, l->chars + c->c + n, (s->ncol - c->c - n) * sizeof(TMTCHAR));

clearline(vt, l, s->ncol - n, s->ncol);
/* VT102 manual says the attribute for the newly empty characters
 * should be the same as the last character moved left, which isn't
 * what clearline() currently does.
 */
}

HANDLER(el)
(void)t;

switch (P0(0)) {
  case 0: clearline(vt, l, c->c, vt->screen.ncol); break;
  case 1: clearline(vt, l, 0, MIN(c->c + 1, s->ncol - 1)); break;
  case 2: clearline(vt, l, 0, vt->screen.ncol); break;
}
}

HANDLER(sgr)
(void)l;
(void)t;
(void)s;
(void)c;

#define FGBG(c) *(P0(i) < 40 ? &vt->attrs.fg : &vt->attrs.bg) = c
for (size_t i = 0; i < vt->npar; i++) switch (P0(i)) {
    case 0: vt->attrs = defattrs; break;
    case 1:
    case 22: vt->attrs.bold = P0(0) < 20; break;
    case 2:
    case 23: vt->attrs.dim = P0(0) < 20; break;
    case 4:
    case 24: vt->attrs.underline = P0(0) < 20; break;
    case 5:
    case 25: vt->attrs.blink = P0(0) < 20; break;
    case 7:
    case 27: vt->attrs.reverse = P0(0) < 20; break;
    case 8:
    case 28: vt->attrs.invisible = P0(0) < 20; break;
    case 10:
    case 11: vt->acs = P0(0) > 10; break;
    case 30:
    case 40: FGBG(TMT_COLOR_BLACK); break;
    case 31:
    case 41: FGBG(TMT_COLOR_RED); break;
    case 32:
    case 42: FGBG(TMT_COLOR_GREEN); break;
    case 33:
    case 43: FGBG(TMT_COLOR_YELLOW); break;
    case 34:
    case 44: FGBG(TMT_COLOR_BLUE); break;
    case 35:
    case 45: FGBG(TMT_COLOR_MAGENTA); break;
    case 36:
    case 46: FGBG(TMT_COLOR_CYAN); break;
    case 37:
    case 47: FGBG(TMT_COLOR_WHITE); break;
    case 39:
    case 49: FGBG(TMT_COLOR_DEFAULT); break;
  }
}

HANDLER(rep)
(void)s;
(void)t;

if (!c->c)
  return;
wchar_t r = l->chars[c->c - 1].c;
for (size_t i = 0; i < P1(0); i++) writecharatcurs(vt, r);
}

HANDLER(dsr)
(void)s;
(void)l;
(void)t;

char r[BUF_MAX + 1] = {0};
snprintf(r, BUF_MAX, "\033[%zd;%zdR", c->r + 1, c->c + 1);
CB(vt, TMT_MSG_ANSWER, (const char *)r);
}

HANDLER(resetparser)
(void)s;
(void)c;
(void)l;
(void)t;

memset(vt->pars, 0, sizeof(vt->pars));
vt->state   = State::S_NUL;
vt->npar    = 0;
vt->arg     = 0;
vt->ignored = false;
}

HANDLER(consumearg)
(void)s;
(void)c;
(void)l;
(void)t;

if (vt->npar < PAR_MAX)
  vt->pars[vt->npar++] = vt->arg;
vt->arg = 0;
}

HANDLER(fixcursor)
(void)l;
(void)t;

c->r = MIN(c->r, s->nline - 1);
c->c = MIN(c->c, s->ncol - 1);
}

#define ON(S, C, A)                       \
  if (vt->state == (S) && strchr(C, i)) { \
    A;                                    \
    return true;                          \
  }
#define DO(S, C, A) \
  ON(               \
      S, C, consumearg(vt); if (!vt->ignored) { A; } fixcursor(vt); resetparser(vt););

static bool handlechar(TMT *vt, char i) {
  COMMON_VARS;

  char cs[] = {i, 0};

  DO(State::S_NUL, "\x07", CB(vt, TMT_MSG_BELL, NULL))
  DO(State::S_NUL, "\x08", if (c->c) c->c--)
  DO(State::S_NUL, "\x09", while (++c->c < s->ncol - 1 && t[c->c].c != L'*'))
  DO(State::S_NUL, "\x0a", c->r < s->nline - 1 ? (void)c->r++ : scrup(vt, 0, 1))
  DO(State::S_NUL, "\x0d", c->c = 0)
  ON(State::S_NUL, "\x1b", vt->state = State::S_ESC)
  ON(State::S_ESC, "\x1b", vt->state = State::S_ESC)
  DO(State::S_ESC, "H", t[c->c].c = L'*')
  DO(State::S_ESC, "7", vt->oldcurs = vt->curs; vt->oldattrs = vt->attrs)
  DO(State::S_ESC, "8", vt->curs = vt->oldcurs; vt->attrs = vt->oldattrs)
  ON(State::S_ESC, "+*()", vt->ignored = true; vt->state = State::S_ARG)
  DO(State::S_ESC, "c", tmt_reset(vt))
  ON(State::S_ESC, "[", vt->state = State::S_ARG)
  ON(State::S_ARG, "\x1b", vt->state = State::S_ESC)
  ON(State::S_ARG, ";", consumearg(vt))
  ON(State::S_ARG, "?", (void)0)
  ON(State::S_ARG, "0123456789", vt->arg = vt->arg * 10 + atoi(cs))
  DO(State::S_ARG, "A", c->r = MAX(c->r - P1(0), 0))
  DO(State::S_ARG, "B", c->r = MIN(c->r + P1(0), s->nline - 1))
  DO(State::S_ARG, "C", c->c = MIN(c->c + P1(0), s->ncol - 1))
  DO(State::S_ARG, "D", c->c = MIN(c->c - P1(0), c->c))
  DO(State::S_ARG, "E", c->c = 0; c->r = MIN(c->r + P1(0), s->nline - 1))
  DO(State::S_ARG, "F", c->c = 0; c->r = MAX(c->r - P1(0), 0))
  DO(State::S_ARG, "G", c->c = MIN(P1(0) - 1, s->ncol - 1))
  DO(State::S_ARG, "d", c->r = MIN(P1(0) - 1, s->nline - 1))
  DO(State::S_ARG, "Hf", c->r = P1(0) - 1; c->c = P1(1) - 1)
  DO(State::S_ARG, "I", while (++c->c < s->ncol - 1 && t[c->c].c != L'*'))
  DO(State::S_ARG, "J", ed(vt))
  DO(State::S_ARG, "K", el(vt))
  DO(State::S_ARG, "L", scrdn(vt, c->r, P1(0)))
  DO(State::S_ARG, "M", scrup(vt, c->r, P1(0)))
  DO(State::S_ARG, "P", dch(vt))
  DO(State::S_ARG, "S", scrup(vt, 0, P1(0)))
  DO(State::S_ARG, "T", scrdn(vt, 0, P1(0)))
  DO(State::S_ARG, "X", clearline(vt, l, c->c, P1(0)))
  DO(State::S_ARG, "Z", while (c->c && t[--c->c].c != L'*'))
  DO(State::S_ARG, "b", rep(vt));
  DO(State::S_ARG, "c", CB(vt, TMT_MSG_ANSWER, "\033[?6c"))
  DO(State::S_ARG, "g", if (P0(0) == 3) clearline(vt, vt->tabs, 0, s->ncol))
  DO(State::S_ARG, "m", sgr(vt))
  DO(State::S_ARG, "n", if (P0(0) == 6) dsr(vt))
  DO(State::S_ARG, "h", if (P0(0) == 25) CB(vt, TMT_MSG_CURSOR, "t"))
  DO(State::S_ARG, "i", (void)0)
  DO(State::S_ARG, "l", if (P0(0) == 25) CB(vt, TMT_MSG_CURSOR, "f"))
  DO(State::S_ARG, "s", vt->oldcurs = vt->curs; vt->oldattrs = vt->attrs)
  DO(State::S_ARG, "u", vt->curs = vt->oldcurs; vt->attrs = vt->oldattrs)
  DO(State::S_ARG, "@", ich(vt))

  return resetparser(vt), false;
}

static void notify(TMT *vt, bool update, bool moved) {
  if (update)
    CB(vt, TMT_MSG_UPDATE, &vt->screen);
  if (moved)
    CB(vt, TMT_MSG_MOVED, &vt->curs);
}

static TMTLINE *allocline(TMT *vt, TMTLINE *o, size_t n, size_t pc) {
  TMTLINE *l = (TMTLINE *)realloc(o, sizeof(TMTLINE) + n * sizeof(TMTCHAR));
  if (!l)
    return NULL;

  clearline(vt, l, pc, n);
  return l;
}

static void freelines(TMT *vt, size_t s, size_t n, bool screen) {
  for (size_t i = s; vt->screen.lines && i < s + n; i++) {
    free(vt->screen.lines[i]);
    vt->screen.lines[i] = NULL;
  }
  if (screen)
    free(vt->screen.lines);
}

TMT *tmt_open(size_t nline, size_t ncol, TMTCALLBACK cb, void *p, const wchar_t *acs) {
  TMT *vt = (TMT *)calloc(1, sizeof(TMT));
  if (!nline || !ncol || !vt) {
    free(vt);
    return NULL;
  }

  /* ASCII-safe defaults for box-drawing characters. */
  vt->acschars = acs ? acs : L"><^v#+:o##+++++~---_++++|<>*!fo";
  vt->cb       = cb;
  vt->p        = p;

  if (!tmt_resize(vt, nline, ncol)) {
    tmt_close(vt);
    return NULL;
  }
  return vt;
}

void tmt_close(TMT *vt) {
  free(vt->tabs);
  freelines(vt, 0, vt->screen.nline, true);
  free(vt);
}

bool tmt_resize(TMT *vt, size_t nline, size_t ncol) {
  if (nline < 2 || ncol < 2)
    return false;
  if (nline < vt->screen.nline)
    freelines(vt, nline, vt->screen.nline - nline, false);

  TMTLINE **l = (TMTLINE **)realloc(vt->screen.lines, nline * sizeof(TMTLINE *));
  if (!l)
    return false;

  size_t pc        = vt->screen.ncol;
  vt->screen.lines = l;
  vt->screen.ncol  = ncol;
  for (size_t i = 0; i < nline; i++) {
    TMTLINE *nl = NULL;
    if (i >= vt->screen.nline)
      nl = vt->screen.lines[i] = allocline(vt, NULL, ncol, 0);
    else
      nl = allocline(vt, vt->screen.lines[i], ncol, pc);

    if (!nl)
      return false;
    vt->screen.lines[i] = nl;
  }
  vt->screen.nline = nline;

  vt->tabs = allocline(vt, vt->tabs, ncol, 0);
  if (!vt->tabs)
    return free(l), false;
  vt->tabs->chars[0].c = vt->tabs->chars[ncol - 1].c = L'*';
  for (size_t i = 0; i < ncol; i++)
    if (i % TAB == 0)
      vt->tabs->chars[i].c = L'*';

  fixcursor(vt);
  dirtylines(vt, 0, nline);
  notify(vt, true, true);
  return true;
}

static void writecharatcurs(TMT *vt, wchar_t w) {
  COMMON_VARS;
  (void)l;
  (void)t;

#ifdef TMT_HAS_WCWIDTH
  extern int wcwidth(wchar_t c);
  if (wcwidth(w) > 1)
    w = TMT_INVALID_CHAR;
  if (wcwidth(w) < 0)
    return;
#endif

  CLINE(vt)->chars[vt->curs.c].c = w;
  CLINE(vt)->chars[vt->curs.c].a = vt->attrs;
  CLINE(vt)->dirty = vt->dirty = true;

  if (c->c < s->ncol - 1)
    c->c++;
  else {
    c->c = 0;
    c->r++;
  }

  if (c->r >= s->nline) {
    c->r = s->nline - 1;
    scrup(vt, 0, 1);
  }
}

static inline size_t testmbchar(TMT *vt) {
  mbstate_t ts = vt->ms;
  return vt->nmb ? mbrtowc(NULL, vt->mb, vt->nmb, &ts) : (size_t)-2;
}

static inline wchar_t getmbchar(TMT *vt) {
  wchar_t c = 0;
  size_t  n = mbrtowc(&c, vt->mb, vt->nmb, &vt->ms);
  vt->nmb   = 0;
  return (n == (size_t)-1 || n == (size_t)-2) ? TMT_INVALID_CHAR : c;
}

void tmt_write(TMT *vt, const char *s, size_t n) {
  TMTPOINT oc = vt->curs;
  n           = n ? n : strlen(s);

  for (size_t p = 0; p < n; p++) {
    if (handlechar(vt, s[p]))
      continue;
    else if (vt->acs)
      writecharatcurs(vt, tacs(vt, (unsigned char)s[p]));
    else if (vt->nmb >= BUF_MAX)
      writecharatcurs(vt, getmbchar(vt));
    else {
      switch (testmbchar(vt)) {
        case (size_t)-1: writecharatcurs(vt, getmbchar(vt)); break;
        case (size_t)-2: vt->mb[vt->nmb++] = s[p]; break;
      }

      if (testmbchar(vt) <= MB_LEN_MAX)
        writecharatcurs(vt, getmbchar(vt));
    }
  }

  notify(vt, vt->dirty, memcmp(&oc, &vt->curs, sizeof(oc)) != 0);
}

const TMTSCREEN *tmt_screen(const TMT *vt) { return &vt->screen; }

const TMTPOINT *tmt_cursor(const TMT *vt) { return &vt->curs; }

void tmt_clean(TMT *vt) {
  for (size_t i = 0; i < vt->screen.nline; i++) vt->dirty = vt->screen.lines[i]->dirty = false;
}

void tmt_reset(TMT *vt) {
  vt->curs.r = vt->curs.c = vt->oldcurs.r = vt->oldcurs.c = vt->acs = (bool)0;
  resetparser(vt);
  vt->attrs = vt->oldattrs = defattrs;
  memset(&vt->ms, 0, sizeof(vt->ms));
  clearlines(vt, 0, vt->screen.nline);
  CB(vt, TMT_MSG_CURSOR, "t");
  notify(vt, true, true);
}
